package com.demon.framework.boot.autoconfigure.cloud;

import com.netflix.appinfo.HealthCheckHandler;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer;
import feign.Client;
import feign.Retryer;
import feign.httpclient.ApacheHttpClient;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.health.HealthAggregator;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EurekaHealthCheckHandler;
import org.springframework.cloud.netflix.ribbon.ServerIntrospector;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory;
import org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer;
import org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer2;
import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * cloud 相关配置
 *
 * @author Demon-HY
 * @date 2020-7-23
 */
@Configuration
@ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)
@AutoConfigureBefore(LoadBalancerAutoConfiguration.class)
@EnableDiscoveryClient //开启 自动发现,在注册中心可看到相关信息
@ConditionalOnClass(name = {"com.netflix.discovery.EurekaClientConfig"})
@EnableFeignClients("com.demon") // 开户 Feign(声明式的web service的客户端)功能
public class CloudAutoConfiguration {

    /**
     * rest 与服务器创建连接的超时时间
     */
    @Value("${cloud.rest.timeout.connect:${ribbon.ConnectTimeout:1000}}")
    private Integer connectTimeout;
    /**
     * rest 从连接池获取连接的timeout
     */
    @Value("${cloud.rest.timeout.connect.request:3000}")
    private Integer connectionRequestTimeout;
    /**
     * rest 读取的超时时间
     */
    @Value("${cloud.rest.timeout.read:${ribbon.ReadTimeout:7000}}")
    private Integer readTimeout;
    /**
     * rest 所有服务的最大访问量的
     */
    @Value("${cloud.rest.max.total:1000}")
    private Integer maxTotal;
    /**
     * rest 每个服务访问的最大连接数
     */
    @Value("${cloud.rest.max.perroute:150}")
    private Integer maxPerRoute;

    @ConditionalOnMissingBean(name = "clientHttpRequestFactory")
    @Bean
    public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {
        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
        connManager.setMaxTotal(maxTotal);
        connManager.setDefaultMaxPerRoute(maxPerRoute);
        HttpComponentsClientHttpRequestFactory clientHttpRequestFactory =
                new HttpComponentsClientHttpRequestFactory(HttpClients.createMinimal(connManager));
        clientHttpRequestFactory.setConnectTimeout(connectTimeout);
        clientHttpRequestFactory.setConnectionRequestTimeout(connectionRequestTimeout);
        clientHttpRequestFactory.setReadTimeout(readTimeout);
        return clientHttpRequestFactory;
    }

    /**
     * 注入一个HttpClient 给Fegin使用 @link org.springframework.cloud.netflix.feign.ribbon.FeignRibbonClientAutoConfiguration
     */
    @ConditionalOnMissingBean(name = "clientHttpRequestFactory")
    @Bean
    public HttpClient httpClient(HttpComponentsClientHttpRequestFactory clientHttpRequestFactory) {
        return clientHttpRequestFactory.getHttpClient();
    }

    /**
     * 取消重试机制,全体取消feign的重试机制，使用robbin重试
     * <p>
     * #ribbon请求连接的超时时间
     * ribbon.ConnectTimeout=250
     * #请求处理的超时时间
     * ribbon.ReadTimeout=1000
     * #对所有请求操作都进行重试
     * ribbon.OkToRetryOnAllOperations=true
     * #对当前服务的重试次数（第一次分配给实例1的时候，如果404，则再重试MaxAutoRetries次，如果还是404，
     * #则切换到其他服务MaxAutoRetriesNextServer决定）
     * ribbon.MaxAutoRetries=0
     * #切换服务的次数(比如本次请求分配给实例1处理，发现404，则切换分配给实例2处理，如果还是404，则返回404给客户端）
     * ribbon.MaxAutoRetriesNextServer=1
     */
    @Bean
    @Primary
    Retryer feignRetryer() {
        return Retryer.NEVER_RETRY;
    }

    /**
     * 实例化 RestTemplate 并让其具备软负载的功能
     * FIXME 对 Fegin 和  Hystrix 无效
     */
    @ConditionalOnMissingBean(name = "loadBalancedRestTemplate")
    @Bean
    @LoadBalanced
    public RestTemplate loadBalancedRestTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
        return createRestTemplate(clientHttpRequestFactory);
    }

    /**
     * 实例化 RestTemplate 不具备软负载的功能
     */
    @ConditionalOnMissingBean(name = "restTemplate")
    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
        return createRestTemplate(clientHttpRequestFactory);
    }

    private RestTemplate createRestTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
        RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
        List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
        restTemplate.setInterceptors(list);

        return restTemplate;
    }

    /**
     * 扩展 出 robbin的重试机制 详见   {@link FeignLoadBalancer2}
     */
    @Configuration
    @ConditionalOnClass(ApacheHttpClient.class)
    @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
    protected static class HttpClientFeignLoadBalancedConfiguration {

        @Autowired(required = false)
        private HttpClient httpClient;

        @Bean
        @ConditionalOnMissingBean(Client.class)
        public Client feignClient(SpringClientFactory clientFactory) {
            ApacheHttpClient delegate;
            if (this.httpClient != null) {
                delegate = new ApacheHttpClient(this.httpClient);
            } else {
                delegate = new ApacheHttpClient();
            }

            CachingSpringLoadBalancerFactory cachingFactory = new CachingSpringLoadBalancerFactory(clientFactory) {
                private final Map<String, FeignLoadBalancer> cache2 = new ConcurrentReferenceHashMap<>();

                @Override
                public FeignLoadBalancer create(String clientName) {
                    if (this.cache2.containsKey(clientName)) {
                        return this.cache2.get(clientName);
                    }
                    IClientConfig config = clientFactory.getClientConfig(clientName);
                    ILoadBalancer lb = clientFactory.getLoadBalancer(clientName);
                    ServerIntrospector serverIntrospector = clientFactory.getInstance(clientName, ServerIntrospector.class);
                    FeignLoadBalancer client = new FeignLoadBalancer2(lb, config, serverIntrospector);
                    this.cache2.put(clientName, client);
                    return client;
                }
            };
            return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);

        }
    }

    /**
     * 开启eureka健康检查 ,默认是关闭
     */
    @Configuration
    @ConditionalOnProperty(value = "eureka.client.healthcheck.enabled", matchIfMissing = true)
    protected static class EurekaHealthCheckHandlerConfiguration {
        private final HealthAggregator healthAggregator;

        @Autowired(required = false)
        public EurekaHealthCheckHandlerConfiguration(HealthAggregator healthAggregator) {
            this.healthAggregator = healthAggregator;
        }

        @Bean
        @ConditionalOnMissingBean(HealthCheckHandler.class)
        public EurekaHealthCheckHandler eurekaHealthCheckHandler() {
            return new EurekaHealthCheckHandler(this.healthAggregator);
        }
    }
}
